﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using Cjc.Calculation.ExpressionEngine.Excel.Compiler;
using System.Linq.Expressions;

namespace Cjc.Calculation.ExpressionEngine
{
	public class Cell : INotifyPropertyChanged, IDisposable
	{
		private class InvalidatedEventArgs : EventArgs
		{
			public bool RaisePropertyChanged { get; private set; }

			public InvalidatedEventArgs( bool raisePropertyChanged )
			{
				this.RaisePropertyChanged = raisePropertyChanged;
			}
		}

		public Worksheet Worksheet { get; private set; }
		public Formula Formula { get; private set; }
		public Exception Error { get; private set; }

		private event EventHandler<InvalidatedEventArgs> Invalidated;

		private volatile bool hasValue = false;
		private volatile bool isDirty = false;
		private volatile bool isBusy = false;

		private object value;
		private Cell[] dependencies = null;
		private Cell[] runtimeDependencies = null;
		private Formula optimizedFormula = null;
		private bool isOptimized = false;

		public Cell( Worksheet worksheet, Formula formula )
		{
			this.Worksheet = worksheet;
			this.Formula = formula;

			formula.PropertyChanged += OnFormulaChanged;
		}

		private void OnFormulaChanged( object sender, PropertyChangedEventArgs e )
		{
			if ( e.PropertyName == "Invoker" )
			{
				isOptimized = false;
				if ( optimizedFormula != null ) Interlocked.Exchange<Formula>( ref optimizedFormula, null );
				RefreshDependencies( ref dependencies, null );
				RefreshDependencies( ref runtimeDependencies, null );
				Invalidate( new InvalidatedEventArgs( false ) ); // Clear value
				Invalidate( new InvalidatedEventArgs( true ) ); // Raise changed
			}
		}

		private void RefreshDependencies( ref Cell[] dependencies, IEnumerable<Cell> replacements )
		{
			var distinct = ( replacements != null )
				? replacements.Distinct().ToArray()
				: null;

			var old = Interlocked.Exchange<Cell[]>(
				ref dependencies,
				distinct );

			if ( distinct != null && distinct.Length > 0 )
			{
				var added = ( old != null )
					? distinct.Except( old )
					: distinct;

				foreach ( var add in added ) add.Invalidated += OnDependencyInvalidated;
			}

			if ( old != null && old.Length > 0 )
			{
				var removed = ( distinct != null )
					? old.Except( distinct )
					: old;

				foreach ( var remove in removed ) remove.Invalidated -= OnDependencyInvalidated;
			}
		}

		public object DisplayValue
		{
			get
			{
				var value = Value;

				if ( value != null && value.GetType() == typeof( double ) && double.IsNaN( (double)value ) )
				{
					value = null;
				}

				return value
					?? ( Formula.NullFormula != null ? Formula.NullFormula.Execute( new WorksheetScope( Worksheet ) ) : null );
			}

			set
			{
				this.Value = value;
			}
		}

		public object Value
		{
			get
			{
				if ( !hasValue )
				{
					if ( isBusy )
					{
						Error = new ApplicationException( "Circular reference detected" );
						return value;
					}

					isBusy = true;

					try
					{
						Interlocked.Exchange<object>( ref value, GetValue() );
						Error = null;
					}
					catch ( Exception ex )
					{
						Interlocked.Exchange<object>( ref value, null );
						Error = ex;
					}

					hasValue = true;
					isBusy = false;
				}

				return value;
			}

			set
			{
				if ( value is string && ( (string)value ).Trim().StartsWith( "=" ) )
				{
					var str = (string)value;
					Formula.ExpressionText = str.Substring( str.IndexOf( '=' ) + 1 );
				}
				else if ( value is string )
				{
					var typedValue = ( (string)value ).ParseValue();
					Formula.Expression = Expression.Constant( typedValue );
				}
				else
				{
					Formula.Expression = Expression.Constant( value );
				}
			}
		}

		private Formula OptimizedFormula
		{
			get
			{
				if ( !isOptimized )
				{
					isOptimized = true;

					var dependencies = new List<Cell>();

					Interlocked.Exchange<Formula>( ref optimizedFormula, OptimizeFormula( Formula, dependencies ) );

					var nestedDependencies = optimizedFormula.NestedDependencies.Select( f => Worksheet[ f ] );

					RefreshDependencies( ref this.dependencies, nestedDependencies.Union( dependencies ) );
				}

				return optimizedFormula;
			}
		}

		protected virtual Formula OptimizeFormula( Formula formula, IList<Cell> dependencies )
		{
			if ( formula.Invoker != null )
			{
				var optimizer = new WorksheetRewriter( Worksheet );
				var optimized = optimizer.Visit( formula.Invoker.Code );

				if ( optimizer.IsOptimized )
				{
					if ( dependencies != null )
					{
						foreach ( var d in optimizer.Dependencies.Distinct() ) dependencies.Add( d );
					}

					return Formula.MakeOptimizedFormula( optimized );
				}
			}

			return formula;
		}

		private void OnDependencyInvalidated( object sender, InvalidatedEventArgs e )
		{
			Invalidate( e );
		}

		private void Invalidate( InvalidatedEventArgs e )
		{
			if ( !e.RaisePropertyChanged && hasValue )
			{
				hasValue = false;
				isDirty = true;
			}
			else if ( e.RaisePropertyChanged && isDirty )
			{
				isDirty = false;
				if ( PropertyChanged != null ) PropertyChanged( this, new PropertyChangedEventArgs( "Value" ) );
			}

			if ( Invalidated != null ) Invalidated( this, e );
		}

		public virtual object GetValue()
		{
			return GetValue( new WorksheetScope( Worksheet ) );
		}

		public virtual object GetValue( WorksheetScope scope )
		{
#if DEBUG
			var start = DateTime.Now;
#endif
			var formula = ( OptimizedFormula != null ) ? optimizedFormula : Formula;
#if DEBUG
			var startExecute = DateTime.Now;
#endif
			var result = formula.Execute( scope );
#if DEBUG
			var finish = DateTime.Now;
#endif

			RefreshDependencies( ref runtimeDependencies, scope.RuntimeDependencies );

#if DEBUG && TRACEGETVALUE
			Debug.WriteLine( string.Format( "{0} = {1} : {2}ms {3} / {4}ms execute : {5}",
				SymbolTable.IdToString( Formula.Symbol ),
				result ?? "(null)",
				( startExecute - start ).TotalMilliseconds,
				"compile",
				( finish - startExecute ).TotalMilliseconds,
				formula.ExpressionText ?? "(None)" ) );
#endif

			return result;
		}

		public string Key
		{
			get { return Formula.Key ?? Formula.Symbol.ToString(); }
		}

		public override string ToString()
		{
			return Key;
		}

		public string DebugDump
		{
			get
			{
				var cells = ( dependencies ?? new Cell[ 0 ] ).Union( runtimeDependencies ?? new Cell[ 0 ] );
				var debug = new StringBuilder();

				foreach ( var cell in cells )
				{
					debug.AppendFormat( "{0}{1}={2}", debug.Length > 0 ? ", " : "", cell.Key, cell.Value );
				}

				return ( debug.Length > 0 ) ? debug.ToString() : null;
			}
		}

		#region INotifyPropertyChanged Members

		public event PropertyChangedEventHandler PropertyChanged;

		#endregion

		#region IDisposable Members

		public void Dispose()
		{
			Formula.PropertyChanged -= OnFormulaChanged;
		}

		#endregion
	}
}